// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;

namespace VirtualStaticInterfaceMethodTestGen
{
    class Program
    {
        enum OperationTested
        {
            Call,
            Ldftn,
            CreateDelegate
        }

        enum ConstrainedTypeDefinition
        {
            NonGenericClass,
            NonGenericValuetype,
            GenericClass,
            GenericValuetype
        }

        enum ConstrainedTypeScenario
        {
            NonGeneric,
            GenericOverStruct,
            GenericOverGenericStructOverTypeParameter,
            GenericOverReferenceType_ClassA,
            GenericOverTypeParameter,
        }

        enum InterfaceType
        {
            NonGeneric,
            GenericOverString,
            GenericOverObject,
            CuriouslyRecurringGeneric
        }

        enum MethodType
        {
            NormalMethod,
            GenericMethodOverInt,
            GenericMethodOverString,
            GenericMethodOverTypeParameter
        }

        enum CallerMethodScenario
        {
            NonGeneric,
            GenericOverInt32,
            GenericOverString,
            GenericOverConstrainedType
        }

        struct TestScenario
        {
            public OperationTested Operation;
            public ConstrainedTypeDefinition ConstrainedTypeDefinition;
            public ConstrainedTypeScenario ConstrainedType;
            public InterfaceType InterfaceType;
            public CallerMethodScenario CallerScenario;
            public MethodType MethodType;

            public override string ToString()
            {
                return $"{Operation}_{ConstrainedType}{ConstrainedTypeDefinition}_{CallerScenario}_{InterfaceType}_{MethodType}";
            }

            public static IEnumerable<TestScenario> GetScenarios()
            {
                foreach (var constaintTypeScenario in (ConstrainedTypeScenario[])typeof(ConstrainedTypeScenario).GetEnumValues())
                    foreach (var callerScenario in (CallerMethodScenario[])typeof(CallerMethodScenario).GetEnumValues())
                        foreach (var interfaceScenario in (InterfaceType[])typeof(InterfaceType).GetEnumValues())
                            foreach (var methodType in (MethodType[])typeof(MethodType).GetEnumValues())
                                foreach (var constrainedTypeDefinition in (ConstrainedTypeDefinition[])typeof(ConstrainedTypeDefinition).GetEnumValues())
                                    foreach (var opTested in (OperationTested[])typeof(OperationTested).GetEnumValues())
                                    {

                                        TestScenario scenario = new TestScenario();
                                        scenario.Operation = opTested;
                                        scenario.ConstrainedType = constaintTypeScenario;
                                        scenario.CallerScenario = callerScenario;
                                        scenario.InterfaceType = interfaceScenario;
                                        scenario.MethodType = methodType;
                                        scenario.ConstrainedTypeDefinition = constrainedTypeDefinition;
                                        yield return scenario;
                                    }
            }
        }

        static void EmitTestGlobalHeader(TextWriter tw)
        {
            tw.WriteLine("// Licensed to the .NET Foundation under one or more agreements.");
            tw.WriteLine("// The .NET Foundation licenses this file to you under the MIT license.");
            tw.WriteLine("");
            tw.WriteLine("// THIS FILE IS AUTOGENERATED EDIT Generator/Program.cs instead and rerun the generator");
            tw.WriteLine(".assembly extern System.Console {}");
            tw.WriteLine(".assembly extern xunit.core {}");
            tw.WriteLine(".assembly extern mscorlib {}");
            tw.WriteLine(".assembly extern System.Runtime {}");
            tw.WriteLine(".assembly extern GenericContextCommonCs {}");
            tw.WriteLine(".assembly extern Microsoft.DotNet.XUnitExtensions { .publickeytoken = (31 BF 38 56 AD 36 4E 35 ) }");
        }

        static void EmitAssemblyExternRecord(TextWriter tw, string assemblyName)
        {
            tw.WriteLine($".assembly extern {assemblyName} {{}}");
        }
        static void EmitAssemblyRecord(TextWriter tw, string assemblyName)
        {
            tw.WriteLine($".assembly {assemblyName} {{}}");
        }

        static void EmitCodeForCommonLibrary(TextWriter tw)
        {
            tw.WriteLine(@".class interface public abstract auto ansi IFaceNonGeneric");
            tw.WriteLine(@"{");
            tw.WriteLine(@"    .method public newslot virtual abstract static void NormalMethod() {}");
            tw.WriteLine(@"    .method public newslot virtual abstract static void GenericMethod<U>() {}");
            tw.WriteLine(@"}");
            tw.WriteLine(@"");
            tw.WriteLine(@".class interface public abstract auto ansi IFaceGeneric`1<T>");
            tw.WriteLine(@"{");
            tw.WriteLine(@"    .method public newslot virtual abstract static void NormalMethod() {}");
            tw.WriteLine(@"    .method public newslot virtual abstract static void GenericMethod<U>() {}");
            tw.WriteLine(@"}");
            tw.WriteLine(@"");
            tw.WriteLine(@".class interface public abstract auto ansi IFaceCuriouslyRecurringGeneric`1<(class IFaceCuriouslyRecurringGeneric`1<!0>) T> ");
            tw.WriteLine(@"{");
            tw.WriteLine(@"    .method public newslot virtual abstract static void NormalMethod() {}");
            tw.WriteLine(@"    .method public newslot virtual abstract static void GenericMethod<U>() {}");
            tw.WriteLine(@"}");
            tw.WriteLine(@"");
            tw.WriteLine(@".class interface public abstract auto ansi IFaceNonGenericDefaultImp");
            tw.WriteLine(@"{");
            tw.WriteLine(@"    .method public newslot virtual static void NormalMethod()");
            tw.WriteLine(@"    {");
            tw.WriteLine(@"        ldstr ""IFaceNonGenericDefaultImp.NormalMethod""");
            tw.WriteLine(@"        stsfld string [GenericContextCommonCs]Statics::String");
            tw.WriteLine(@"        ret");
            tw.WriteLine(@"    }");
            tw.WriteLine(@"    .method public newslot virtual static void GenericMethod<U>()");
            tw.WriteLine(@"    {");
            tw.WriteLine(@"        ldstr ""IFaceNonGenericDefaultImp.GenericMethod<""");
            tw.WriteLine(@"        ldtoken !!0");
            tw.WriteLine(@"        call string[GenericContextCommonCs] Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
            tw.WriteLine(@"        ldstr "">""");
            tw.WriteLine(@"        call string[System.Runtime] System.String::Concat(string, string, string)");
            tw.WriteLine(@"        stsfld string [GenericContextCommonCs]Statics::String");
            tw.WriteLine(@"        ret");
            tw.WriteLine(@"    }");
            tw.WriteLine(@"}");
            tw.WriteLine(@"");
            tw.WriteLine(@".class interface public abstract auto ansi IFaceGenericDefaultImp`1<T>");
            tw.WriteLine(@"{");
            tw.WriteLine(@"    .method public newslot virtual static void NormalMethod()");
            tw.WriteLine(@"    {");
            tw.WriteLine(@"        ldstr ""IFaceGenericDefaultImp`1<""");
            tw.WriteLine(@"        ldtoken !0");
            tw.WriteLine(@"        call string[GenericContextCommonCs] Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
            tw.WriteLine(@"        ldstr "">.NormalMethod""");
            tw.WriteLine(@"        call string[System.Runtime] System.String::Concat(string, string, string)");
            tw.WriteLine(@"        stsfld string [GenericContextCommonCs]Statics::String");
            tw.WriteLine(@"        ret");
            tw.WriteLine(@"    }");
            tw.WriteLine(@"    .method public newslot virtual static void GenericMethod<U>()");
            tw.WriteLine(@"    {");
            tw.WriteLine(@"        ldstr ""IFaceGenericDefaultImp`1<""");
            tw.WriteLine(@"        ldtoken !0");
            tw.WriteLine(@"        call string[GenericContextCommonCs] Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
            tw.WriteLine(@"        ldstr "">.GenericMethod<""");
            tw.WriteLine(@"        ldtoken !!0");
            tw.WriteLine(@"        call string[GenericContextCommonCs] Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
            tw.WriteLine(@"        ldstr "">""");
            tw.WriteLine(@"        call string[System.Runtime] System.String::Concat(string, string, string)");
            tw.WriteLine(@"        call string[System.Runtime] System.String::Concat(string, string, string)");
            tw.WriteLine(@"        stsfld string [GenericContextCommonCs]Statics::String");
            tw.WriteLine(@"        ret");
            tw.WriteLine(@"    }");
            tw.WriteLine(@"}");
            tw.WriteLine(@"");
            tw.WriteLine(@".class interface public abstract auto ansi IFaceCuriouslyRecurringGenericDefaultImp`1<(class IFaceCuriouslyRecurringGenericDefaultImp`1<!0>) T> ");
            tw.WriteLine(@"{");
            tw.WriteLine(@"    .method public newslot virtual static void NormalMethod()");
            tw.WriteLine(@"    {");
            tw.WriteLine(@"        ldstr ""IFaceCuriouslyRecurringGenericDefaultImp`1<""");
            tw.WriteLine(@"        ldtoken !0");
            tw.WriteLine(@"        call string[GenericContextCommonCs] Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
            tw.WriteLine(@"        ldstr "">.NormalMethod""");
            tw.WriteLine(@"        call string[System.Runtime] System.String::Concat(string, string, string)");
            tw.WriteLine(@"        stsfld string [GenericContextCommonCs]Statics::String");
            tw.WriteLine(@"        ret");
            tw.WriteLine(@"    }");
            tw.WriteLine(@"    .method public newslot virtual static void GenericMethod<U>()");
            tw.WriteLine(@"    {");
            tw.WriteLine(@"        ldstr ""IFaceCuriouslyRecurringGenericDefaultImp`1<""");
            tw.WriteLine(@"        ldtoken !0");
            tw.WriteLine(@"        call string[GenericContextCommonCs] Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
            tw.WriteLine(@"        ldstr "">.GenericMethod<""");
            tw.WriteLine(@"        ldtoken !!0");
            tw.WriteLine(@"        call string[GenericContextCommonCs] Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
            tw.WriteLine(@"        ldstr "">""");
            tw.WriteLine(@"        call string[System.Runtime] System.String::Concat(string, string, string)");
            tw.WriteLine(@"        call string[System.Runtime] System.String::Concat(string, string, string)");
            tw.WriteLine(@"        stsfld string [GenericContextCommonCs]Statics::String");
            tw.WriteLine(@"        ret");
            tw.WriteLine(@"    }");

            tw.WriteLine(@"}");
            tw.WriteLine(@"");
            tw.WriteLine(@".class public sealed auto ansi GenericStruct`1<T>");
            tw.WriteLine(@"       extends[System.Runtime] System.ValueType");
            tw.WriteLine(@"{");
            tw.WriteLine(@"}");
        }


        public struct ClassDesc
        {
            public string BaseType;
            public string ClassFlags;
            public string GenericParams;
            public string Name;
            public IEnumerable<string> InterfacesImplemented;
        }

        static void EmitClass(TextWriter tw, ClassDesc clz)
        {
            string genericParamString = "";
            if (clz.GenericParams != null)
                genericParamString = $"<{clz.GenericParams}>";
            tw.WriteLine($".class {clz.ClassFlags} {clz.Name}{genericParamString}");
            if (clz.BaseType != null)
            {
                tw.WriteLine($"       extends {clz.BaseType}");
            }

            if (clz.InterfacesImplemented != null)
            {
                bool first = true;
                foreach (string iface in clz.InterfacesImplemented)
                {
                    if (first)
                    {
                        first = false;
                        tw.Write("       implements ");
                    }
                    else
                    {
                        tw.Write(","+ Environment.NewLine + "                  ");
                    }
                    tw.Write(iface);
                }

                if (first == true)
                {
                    throw new Exception();
                }
                tw.WriteLine("");
            }
            tw.WriteLine("{");
        }

        static void EmitEndClass(TextWriter tw, ClassDesc clz)
        {
            tw.WriteLine($"}} // end of class {clz.Name}");
        }

        public struct MethodDesc
        {
            public string Name;
            public string Arguments;
            public string ReturnType;
            public bool HasBody;
            public IEnumerable<string> MethodImpls;
            public string MethodFlags;
        }

        static string ToILDasmTypeName(string typeName, string instantiation)
        {
            if (instantiation != "")
            {
                return $"class {typeName}{instantiation}";
            }
            else
            {
                return typeName;
            }
        }

        static void EmitMethod(TextWriter tw, MethodDesc md)
        {
            tw.WriteLine($"  .method { md.MethodFlags} {md.ReturnType} {md.Name}({md.Arguments}) cil managed noinlining");
            tw.WriteLine("  {");
            if (md.MethodImpls != null)
            {
                foreach (var methodImpl in md.MethodImpls)
                {
                    tw.WriteLine($"    .override {methodImpl}");
                }
            }
        }

        static void EmitEndMethod(TextWriter tw, MethodDesc md)
        {
            tw.WriteLine($"  }} // end of method {md.Name}");
        }

        static void GenerateImplementations(TextWriter tw, bool testDefaultImplementations, string defaultImpSuffix)
        {
            foreach (var constrainedTypeDefinition in (ConstrainedTypeDefinition[])typeof(ConstrainedTypeDefinition).GetEnumValues())
            {
                string baseType = constrainedTypeDefinition.ToString().Contains("Valuetype") ? "[System.Runtime]System.ValueType" : "[System.Runtime]System.Object";

                ClassDesc implClass = new ClassDesc();
                implClass.BaseType = baseType;

                implClass.Name = GetConstrainedTypeName(constrainedTypeDefinition, false);
                implClass.ClassFlags = "public auto ansi";
                string implTypePrefix = "class";
                if (constrainedTypeDefinition.ToString().Contains("Valuetype"))
                {
                    implTypePrefix = "valuetype";
                    implClass.ClassFlags = implClass.ClassFlags + " sealed";
                }

                if (constrainedTypeDefinition.ToString().StartsWith("Generic"))
                    implClass.GenericParams = "T";

                List<string> interfacesImplemented = new List<string>();
                StringWriter implsGenerated = new StringWriter();
                implClass.InterfacesImplemented = interfacesImplemented;

                if (constrainedTypeDefinition.ToString().StartsWith("Generic"))
                {
                    GenerateImpl(implClass.Name + "<!0>", $"IFaceNonGeneric{defaultImpSuffix}", "", false);
                    GenerateImpl(implClass.Name + "<!0>", $"IFaceGeneric{defaultImpSuffix}`1", "<string>", true);
                    GenerateImpl(implClass.Name + "<!0>", $"IFaceGeneric{defaultImpSuffix}`1", "<object>", true);
                    GenerateImpl(implClass.Name + "<!0>", $"IFaceCuriouslyRecurringGeneric{defaultImpSuffix}`1", $"<{implTypePrefix} {implClass.Name}<!0>>", true);
                }
                else
                {
                    GenerateImpl(implClass.Name, $"IFaceNonGeneric{defaultImpSuffix}", "", false);
                    GenerateImpl(implClass.Name, $"IFaceGeneric{defaultImpSuffix}`1", "<string>", true);
                    GenerateImpl(implClass.Name, $"IFaceGeneric{defaultImpSuffix}`1", "<object>", true);
                    GenerateImpl(implClass.Name, $"IFaceCuriouslyRecurringGeneric{defaultImpSuffix}`1", $"<{implTypePrefix} {implClass.Name}>", true);
                }

                EmitClass(tw, implClass);
                if (!constrainedTypeDefinition.ToString().Contains("Valuetype"))
                {
                    tw.WriteLine($"  .method public hidebysig specialname rtspecialname ");
                    tw.WriteLine($"          instance void  .ctor() cil managed");
                    tw.WriteLine($"  {{");
                    tw.WriteLine($"    .maxstack  8");
                    tw.WriteLine($"    IL_0000:  ldarg.0");
                    tw.WriteLine($"    IL_0001:  call       instance void {implClass.BaseType}::.ctor()");
                    tw.WriteLine($"    IL_0006:  ret");
                    tw.WriteLine($"  }}");
                }
                tw.WriteLine(implsGenerated.ToString());
                EmitEndClass(tw, implClass);

                void GenerateImpl(string implType, string iface, string ifaceGenericArguments, bool isGeneric)
                {
                    interfacesImplemented.Add(ToILDasmTypeName(iface, ifaceGenericArguments));

                    if (testDefaultImplementations)
                        return;

                    MethodDesc implMethodDesc = new MethodDesc();
                    implMethodDesc.Name = $"'{iface}{ifaceGenericArguments}.NormalMethod'";
                    implMethodDesc.MethodFlags = "public static";
                    implMethodDesc.ReturnType = "void";
                    implMethodDesc.HasBody = true;
                    implMethodDesc.MethodImpls = new string[] { $"method void {ToILDasmTypeName(ImplPrefix+iface, ifaceGenericArguments)}::NormalMethod()" };

                    EmitMethod(implsGenerated, implMethodDesc);
                    GenerateMethodBody(false);
                    EmitEndMethod(implsGenerated, implMethodDesc);

                    implMethodDesc.Name = $"'{iface}{ifaceGenericArguments}.GenericMethod'<U>";
                    implMethodDesc.MethodImpls = new string[] { $"method void {ToILDasmTypeName(ImplPrefix + iface, ifaceGenericArguments)}::GenericMethod<[1]>()" };
                    EmitMethod(implsGenerated, implMethodDesc);
                    GenerateMethodBody(true);
                    EmitEndMethod(implsGenerated, implMethodDesc);

                    void GenerateMethodBody(bool genericMethod)
                    {
                        implsGenerated.WriteLine($"    ldtoken {implTypePrefix} {implType}");
                        implsGenerated.WriteLine($"    call string {CommonCsPrefix}Statics::MakeName(valuetype [System.Runtime]System.RuntimeTypeHandle)");

                        string methodNameToEmit = implMethodDesc.Name;
                        if (methodNameToEmit.EndsWith("<U>"))
                        {
                            methodNameToEmit = methodNameToEmit.Substring(0, methodNameToEmit.Length - 3);
                        }
                        implsGenerated.WriteLine($"    ldstr \"{methodNameToEmit}\"");
                        if (methodNameToEmit.Contains("!!0"))
                        {
                            implsGenerated.WriteLine($"    ldstr \"!!0\"");
                            implsGenerated.WriteLine($"    ldtoken !!0");
                            implsGenerated.WriteLine($"    call string {CommonCsPrefix}Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
                            implsGenerated.WriteLine($"    call instance string [System.Runtime]System.String::Replace(string, string)");
                        }
                        if (methodNameToEmit.Contains("!!1"))
                        {
                            implsGenerated.WriteLine($"    ldstr \"!!1\"");
                            implsGenerated.WriteLine($"    ldtoken !!1");
                            implsGenerated.WriteLine($"    call string {CommonCsPrefix}Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
                            implsGenerated.WriteLine($"    call instance string [System.Runtime]System.String::Replace(string, string)");
                        }
                        if (methodNameToEmit.Contains("!0"))
                        {
                            implsGenerated.WriteLine($"    ldstr \"!0\"");
                            implsGenerated.WriteLine($"    ldtoken !0");
                            implsGenerated.WriteLine($"    call string {CommonCsPrefix}Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
                            implsGenerated.WriteLine($"    call instance string [System.Runtime]System.String::Replace(string, string)");
                        }
                        if (genericMethod)
                        {
                            implsGenerated.WriteLine($"    ldstr \"<\"");
                            implsGenerated.WriteLine($"    ldtoken !!0");
                            implsGenerated.WriteLine($"    call string {CommonCsPrefix}Statics::MakeName(valuetype[System.Runtime]System.RuntimeTypeHandle)");
                            implsGenerated.WriteLine($"    ldstr \">\"");
                            implsGenerated.WriteLine($"    call string[System.Runtime] System.String::Concat(string, string, string,string)");
                        }
                        implsGenerated.WriteLine($"    call string[System.Runtime] System.String::Concat(string, string)");
                        implsGenerated.WriteLine($"    stsfld string {CommonCsPrefix}Statics::String");
                        implsGenerated.WriteLine($"    ret");
                    }
                }
            }
        }

        static string GetConstrainedTypeName(ConstrainedTypeDefinition isStruct, bool withImplPrefix = true)
        {
            string constrainedType = isStruct.ToString();
            if (constrainedType.StartsWith("Generic"))
            {
                constrainedType = constrainedType + "`1";
            }
            if (withImplPrefix)
                return ImplPrefix + constrainedType;
            else
                return constrainedType;
        }

        static string CommonCsAssemblyName = "GenericContextCommonCs";
        static string CommonAndImplAssemblyName = "GenericContextCommonAndImplementation";
        static string CommonPrefix = $"[{CommonAndImplAssemblyName}]";
        static string CommonCsPrefix = $"[{CommonCsAssemblyName}]";
        static string ImplPrefix = $"[{CommonAndImplAssemblyName}]";


        static string AppendSuffixToConstrainedType(TestScenario scenario, string constrainedType, out bool invalidScenario)
        {
            invalidScenario = false;
            switch (scenario.ConstrainedType)
            {
                case ConstrainedTypeScenario.NonGeneric:
                    if (constrainedType.StartsWith(ImplPrefix + "Generic"))
                        invalidScenario = true;

                    break;

                case ConstrainedTypeScenario.GenericOverTypeParameter:
                    if (!constrainedType.StartsWith(ImplPrefix + "Generic"))
                        invalidScenario = true;
                    if (scenario.CallerScenario == CallerMethodScenario.NonGeneric)
                        invalidScenario = true;
                    if (scenario.CallerScenario == CallerMethodScenario.GenericOverConstrainedType)
                        invalidScenario = true;

                    constrainedType = constrainedType + "<!!0>";
                    break;

                case ConstrainedTypeScenario.GenericOverStruct:
                    if (!constrainedType.StartsWith(ImplPrefix + "Generic"))
                        invalidScenario = true;
                    constrainedType = constrainedType + "<int32>";
                    break;

                case ConstrainedTypeScenario.GenericOverReferenceType_ClassA:
                    if (!constrainedType.StartsWith(ImplPrefix + "Generic"))
                        invalidScenario = true;
                    constrainedType = constrainedType + "<object>";
                    break;

                case ConstrainedTypeScenario.GenericOverGenericStructOverTypeParameter:
                    if (!constrainedType.StartsWith(ImplPrefix + "Generic"))
                        invalidScenario = true;
                    if (scenario.CallerScenario == CallerMethodScenario.NonGeneric)
                        invalidScenario = true;
                    if (scenario.CallerScenario == CallerMethodScenario.GenericOverConstrainedType)
                        invalidScenario = true;
                    constrainedType = constrainedType + $"<valuetype {CommonPrefix}GenericStruct`1<!!0>>";
                    break;
                default:
                    throw new Exception("Unexpected value");
            }

            return constrainedType;
        }


        static void Main(string[] args)
        {
            for (int i = 0; i < 3; i++)
            {
                string TestAssemblyName = "GenericContextTest";
                bool testDefaultImplementations = (i == 1) ? true : false;
                string defaultImpSuffix = "";

                if (i != 0)
                    defaultImpSuffix = "DefaultImp";

                TestAssemblyName = TestAssemblyName + defaultImpSuffix;
                if (testDefaultImplementations)
                {
                    TestAssemblyName = TestAssemblyName + "CallDefaultImp";
                }

                string outputDir = Path.Combine(args[0], TestAssemblyName);
                Directory.CreateDirectory(outputDir);

                using StreamWriter twOutputCommon = new StreamWriter(Path.Combine(outputDir, @$"{CommonAndImplAssemblyName}.il"));
                using StreamWriter twOutputTest = new StreamWriter(Path.Combine(outputDir, @$"{TestAssemblyName}.il"));

                StringWriter swMainMethodBody = new StringWriter();
                StringWriter swTestClassMethods = new StringWriter();

                EmitTestGlobalHeader(twOutputCommon);
                EmitAssemblyRecord(twOutputCommon, CommonAndImplAssemblyName);
                EmitCodeForCommonLibrary(twOutputCommon);
                GenerateImplementations(twOutputCommon, testDefaultImplementations, defaultImpSuffix);

                EmitTestGlobalHeader(twOutputTest);
                EmitAssemblyExternRecord(twOutputTest, CommonAndImplAssemblyName);
                EmitAssemblyRecord(twOutputTest, TestAssemblyName);

                foreach (var scenario in TestScenario.GetScenarios())
                {
                    string scenarioName = scenario.ToString();

                    string constrainedType = AppendSuffixToConstrainedType(scenario, GetConstrainedTypeName(scenario.ConstrainedTypeDefinition), out bool skipScenario);
                    if (skipScenario)
                        continue;

                    string interfaceTypeSansImplPrefix;
                    string interfaceMethod;

                    string constrainedTypePrefix;
                    if (constrainedType.Contains("Valuetype"))
                        constrainedTypePrefix = $"valuetype ";
                    else
                        constrainedTypePrefix = $"class ";

                    switch (scenario.InterfaceType)
                    {
                        case InterfaceType.NonGeneric:
                            interfaceTypeSansImplPrefix = $"IFaceNonGeneric{defaultImpSuffix}";
                            break;
                        case InterfaceType.GenericOverString:
                            if (scenario.CallerScenario == CallerMethodScenario.GenericOverConstrainedType)
                                interfaceTypeSansImplPrefix = $"IFaceGeneric{defaultImpSuffix}`1<!!1>";
                            else
                                interfaceTypeSansImplPrefix = $"IFaceGeneric{defaultImpSuffix}`1<string>";
                            break;
                        case InterfaceType.GenericOverObject:
                            if (scenario.CallerScenario == CallerMethodScenario.GenericOverConstrainedType)
                                interfaceTypeSansImplPrefix = $"IFaceGeneric{defaultImpSuffix}`1<!!1>";
                            else
                                interfaceTypeSansImplPrefix = $"IFaceGeneric{defaultImpSuffix}`1<object>";
                            break;
                        case InterfaceType.CuriouslyRecurringGeneric:
                            interfaceTypeSansImplPrefix = $"IFaceCuriouslyRecurringGeneric{defaultImpSuffix}`1<{constrainedTypePrefix}{constrainedType}>";
                            break;
                        default:
                            throw new Exception("Unexpected value");
                    }

                    string interfaceMethodRoot;
                    string interfaceMethodInstantiation = "";

                    switch (scenario.MethodType)
                    {
                        case MethodType.NormalMethod:
                            interfaceMethodRoot = "NormalMethod";
                            break;

                        case MethodType.GenericMethodOverInt:
                            interfaceMethodRoot = "GenericMethod";
                            interfaceMethodInstantiation = "<int32>";
                            break;

                        case MethodType.GenericMethodOverString:
                            interfaceMethodRoot = "GenericMethod";
                            interfaceMethodInstantiation = "<string>";
                            break;

                        case MethodType.GenericMethodOverTypeParameter:
                            interfaceMethodRoot = "GenericMethod";
                            if (scenario.CallerScenario == CallerMethodScenario.NonGeneric)
                                continue;
                            interfaceMethodInstantiation = "<!!0>";
                            break;

                        default:
                            throw new Exception("Unexpected");
                    }

                    interfaceMethod = interfaceMethodRoot + interfaceMethodInstantiation;

                    TextWriter twIL;

                    MethodDesc mdIndividualTestMethod = new MethodDesc();
                    string basicTestMethodName = $"Test_{scenarioName}";
                    mdIndividualTestMethod.Name = basicTestMethodName;
                    mdIndividualTestMethod.HasBody = true;
                    mdIndividualTestMethod.MethodFlags = "public static";
                    mdIndividualTestMethod.MethodImpls = null;
                    mdIndividualTestMethod.ReturnType = "void";
                    mdIndividualTestMethod.Arguments = "";


                    string expectedString = constrainedTypePrefix + AppendSuffixToConstrainedType(scenario, GetConstrainedTypeName(scenario.ConstrainedTypeDefinition, withImplPrefix: false), out _) + "'" + interfaceTypeSansImplPrefix + "." + interfaceMethodRoot + "'" + interfaceMethodInstantiation;
                    expectedString = expectedString.Replace(ImplPrefix, "");

                    if (testDefaultImplementations)
                        expectedString = (interfaceTypeSansImplPrefix + "." + interfaceMethodRoot + interfaceMethodInstantiation).Replace(ImplPrefix, "");

                    if (scenario.CallerScenario == CallerMethodScenario.NonGeneric)
                    {
                        EmitTestMethod();
                        CallTestEntrypoint($"        call void TestEntrypoint::{mdIndividualTestMethod.Name}()");
                    }
                    else
                    {
                        string methodInstantiation;
                        switch (scenario.CallerScenario)
                        {
                            case CallerMethodScenario.GenericOverInt32:
                            case CallerMethodScenario.GenericOverString:

                                mdIndividualTestMethod.Name = mdIndividualTestMethod.Name + "<T>";
                                methodInstantiation = "string";
                                if (scenario.CallerScenario == CallerMethodScenario.GenericOverInt32)
                                    methodInstantiation = "int32";

                                expectedString = expectedString.Replace("!!0", $"{methodInstantiation}");
                                expectedString = expectedString.Replace(ImplPrefix, "");
                                EmitTestMethod();

                                CallTestEntrypoint($"        call void TestEntrypoint::{basicTestMethodName}<{methodInstantiation}>()");
                                break;

                            case CallerMethodScenario.GenericOverConstrainedType:
                                mdIndividualTestMethod.Name = $"{mdIndividualTestMethod.Name}<({(interfaceTypeSansImplPrefix.Contains("`") ? "class " : "")}{CommonPrefix}{interfaceTypeSansImplPrefix}) T,U>";

                                expectedString = expectedString.Replace("!!0", $"{constrainedTypePrefix}{constrainedType}");
                                expectedString = expectedString.Replace(ImplPrefix, "");

                                EmitTestMethod();

                                string callCommand = GetSetBangBang1IfNeeded("string") + $"    call void TestEntrypoint::{basicTestMethodName}<{constrainedTypePrefix}{constrainedType},string>()";
                                if (scenario.InterfaceType == InterfaceType.GenericOverObject)
                                    callCommand = callCommand + Environment.NewLine + GetSetBangBang1IfNeeded("object") + $"        call void TestEntrypoint::{basicTestMethodName}<{constrainedTypePrefix}{constrainedType},object>()";
                                CallTestEntrypoint(callCommand);

                                string GetSetBangBang1IfNeeded(string bangBang1Expected)
                                {
                                    if (expectedString.Contains("!!1"))
                                    {
                                        return $"    ldstr \"{bangBang1Expected}\"" + Environment.NewLine + "    stsfld string [GenericContextCommonCs]Statics::BangBang1Param" + Environment.NewLine;
                                    }
                                    else
                                    {
                                        return "";
                                    }
                                }
                                break;
                            default:
                                throw new Exception("AACLL");
                        }
                    }

                    void CallTestEntrypoint(string callCommand)
                    {
                        swMainMethodBody.WriteLine("    .try {");
                        swMainMethodBody.WriteLine(callCommand);
                        swMainMethodBody.WriteLine($"        leave.s {scenarioName}Done");
                        swMainMethodBody.WriteLine("    } catch [System.Runtime]System.Exception {");
                        swMainMethodBody.WriteLine($"        stloc.0");
                        swMainMethodBody.WriteLine($"        ldstr \"{scenarioName}\"");
                        swMainMethodBody.WriteLine($"        ldstr \"{expectedString}\"");
                        swMainMethodBody.WriteLine($"        ldloc.0");
                        swMainMethodBody.WriteLine($"        callvirt   instance string [System.Runtime]System.Object::ToString()");
                        swMainMethodBody.WriteLine($"        call void [GenericContextCommonCs]Statics::CheckForFailure(string,string,string)");
                        swMainMethodBody.WriteLine($"        leave.s {scenarioName}Done");
                        swMainMethodBody.WriteLine("    }");
                        swMainMethodBody.WriteLine($"{scenarioName}Done: nop");
                    }

                    // If test scenario requires generic class caller, Create Caller class and make a global method method which calls it
                    // If test scenario requires generic method caller, create global generic method as required and non-generic test method
                    // If test scenario requires non-generic method caller, just make global method as caller
                    //   Call callee
                    //
                    // Create Callee class
                    //   With callee method that implements scenario
                    //   fill expected value static with string computed based on scenario + exact type of calle class/generic args of callee method
                    // compute expected result string

                    void EmitTestMethod()
                    {
                        EmitMethod(swTestClassMethods, mdIndividualTestMethod);
                        EmitILToCallActualMethod(swTestClassMethods);
                        swTestClassMethods.WriteLine($"    ldstr \"{scenario.ToString()}\"");
                        swTestClassMethods.WriteLine($"    ldstr \"{expectedString}\"");
                        if (expectedString.Contains("!!1"))
                        {
                            swTestClassMethods.WriteLine("    ldstr \"!!1\"");
                            swTestClassMethods.WriteLine("    ldsfld string [GenericContextCommonCs]Statics::BangBang1Param");
                            swTestClassMethods.WriteLine("    call instance string [System.Runtime]System.String::Replace(string, string)");
                        }
                        swTestClassMethods.WriteLine($"    call void {CommonCsPrefix}Statics::CheckForFailure(string,string)");
                        swTestClassMethods.WriteLine($"    ret");
                        twIL = swTestClassMethods;
                        EmitEndMethod(swTestClassMethods, mdIndividualTestMethod);
                    }
                    void EmitILToCallActualMethod(TextWriter twActualIL)
                    {
                        // Emit the IL to call the actual method
                        switch (scenario.Operation)
                        {
                            case OperationTested.Call:
                                EmitConstrainedPrefix();
                                twActualIL.WriteLine($"    call void class {ImplPrefix}{interfaceTypeSansImplPrefix}::{interfaceMethod}()");
                                break;

                            case OperationTested.Ldftn:
                                EmitConstrainedPrefix();
                                twActualIL.WriteLine($"    ldftn void class {ImplPrefix}{interfaceTypeSansImplPrefix}::{interfaceMethod}()");
                                twActualIL.WriteLine($"    volatile.");
                                twActualIL.WriteLine($"    stsfld     native int modreq([System.Runtime]System.Runtime.CompilerServices.IsVolatile) {CommonCsPrefix}Statics::FtnHolder");
                                twActualIL.WriteLine($"    volatile.");
                                twActualIL.WriteLine($"    ldsfld     native int modreq([System.Runtime]System.Runtime.CompilerServices.IsVolatile) {CommonCsPrefix}Statics::FtnHolder");
                                twActualIL.WriteLine($"    calli      void()");
                                break;

                            case OperationTested.CreateDelegate:
                                twActualIL.WriteLine("    ldnull");
                                EmitConstrainedPrefix();
                                twActualIL.WriteLine($"    ldftn void class {ImplPrefix}{interfaceTypeSansImplPrefix}::{interfaceMethod}()");
                                twActualIL.WriteLine($"    newobj instance void [System.Runtime]System.Action::.ctor(object,");
                                twActualIL.WriteLine($"                                                              native int)");
                                twActualIL.WriteLine($"    volatile.");
                                twActualIL.WriteLine($"    stsfld     class [System.Runtime] System.Action modreq([System.Runtime] System.Runtime.CompilerServices.IsVolatile) {CommonCsPrefix}Statics::ActionHolder");
                                twActualIL.WriteLine($"    volatile.");
                                twActualIL.WriteLine($"    ldsfld class [System.Runtime] System.Action modreq([System.Runtime] System.Runtime.CompilerServices.IsVolatile) {CommonCsPrefix}Statics::ActionHolder");
                                twActualIL.WriteLine($"    callvirt instance void[System.Runtime] System.Action::Invoke()");
                                break;

                            default:
                                throw new Exception();
                        }

                        void EmitConstrainedPrefix()
                        {
                            if (scenario.CallerScenario == CallerMethodScenario.GenericOverConstrainedType)
                                twActualIL.WriteLine($"    constrained. !!0");
                            else
                                twActualIL.WriteLine($"    constrained. {constrainedTypePrefix}{constrainedType}");
                        }
                    }
                }

                ClassDesc mainClass = new ClassDesc();
                mainClass.BaseType = "[System.Runtime]System.Object";
                mainClass.ClassFlags = "public auto ansi";
                mainClass.Name = "TestEntrypoint";

                EmitClass(twOutputTest, mainClass);

                twOutputTest.Write(swTestClassMethods.ToString());

                MethodDesc mainMethod = new MethodDesc();
                mainMethod.Name = "Main";
                mainMethod.Arguments = "";
                mainMethod.ReturnType = "int32";
                mainMethod.MethodImpls = null;
                mainMethod.HasBody = true;
                mainMethod.MethodFlags = "public static";

                EmitMethod(twOutputTest, mainMethod);
                twOutputTest.WriteLine("    .custom instance void [xunit.core]Xunit.FactAttribute::.ctor() = (");
                twOutputTest.WriteLine("        01 00 00 00");
                twOutputTest.WriteLine("    )");
                twOutputTest.WriteLine("    // [SkipOnCoreClr(\"GC Stress Incompatible\", RuntimeTestModes.AnyGCStress)]");
                twOutputTest.WriteLine("    .custom instance void [Microsoft.DotNet.XUnitExtensions]Xunit.SkipOnCoreClrAttribute::.ctor(string, valuetype [Microsoft.DotNet.XUnitExtensions]Xunit.RuntimeTestModes) = (");
                twOutputTest.WriteLine("        01 00 16 47 43 20 53 74 72 65 73 73 20 49 6e 63");
                twOutputTest.WriteLine("        6f 6d 70 61 74 69 62 6c 65 c0 00 00 00 00 00");
	            twOutputTest.WriteLine("    )");
                twOutputTest.WriteLine("    .entrypoint");
                twOutputTest.WriteLine("    .locals init (class [System.Runtime]System.Exception V_0)");
                twOutputTest.Write(swMainMethodBody.ToString());
                twOutputTest.WriteLine($"    call int32 { CommonCsPrefix}Statics::ReportResults()");
                twOutputTest.WriteLine("    ret");

                EmitEndMethod(twOutputTest, mainMethod);
                EmitEndClass(twOutputTest, mainClass);
            }
        }
    }
}
